Modern C++学习心得笔记

本文内容来源于:GitHub - CnTransGroup/EffectiveModernCppChinese: 《Effective Modern C++》- 完成翻译

1. 优先考虑auto而非显式类型声明

简化代码,避免一些移植和效率性问题,不过注意auto推导若非己愿,使用显式类型初始化惯用法

2. 区别() {}方式创建对象

后者调用的是std::initializer_list方法,前者是构造函数。

3. 指针初始化优先使用nullptr而非0或者NULL

nullptr显式指明它是指针类型的空值,0/NULL存在隐式转换的可能性,表达含义前者更为清晰。

4. 优先考虑using而非typedefs定义类型别名

using更加强大支持模板化,使用更简洁。

5. 优先使用限定域enum::Color而非Color

后者读代码时,很难确定作用域。

6. 优先使用deleted关键词禁用函数(拷贝构造和赋值操作)

相比private方式定义更加清晰,避免友元函数也能访问的Bug。

7. 优先使用override关键词声明重写虚函数

虚函数重写的规则很多,一不留神不满足虚函数重写就犯错了,为了强制编译器检查重写情况,可以加override关键词。

8. noexcept声明对于移动语义、swap、内存释放函数和析构函数非常有用

声明noexcept编译器会少做许多事情,提高效率和性能。

简化调用代码,确认不抛出异常的情况下,也能执行一些特殊优化操作。

当vector容量调整时,需要移动元素,如果元素的移动拷贝构造函数是noexcept的,那么可以采用move的方式,而不是copy方式,大幅提高效率。

应用在移动构造函数、移动复制函数、swap函数场景。

9. 尽量使用constexpr

constexpr修饰函数或者表达式,可以将函数或者表达式的结果推导为常量,从而用在一些常量限定域的使用场景中,比如数组的维度、模版的参数等。
另外是一种很强的约束,更好地保证程序的正确语义不被破坏。
比如将用到的constexpr表达式都直接替换成最终结果等。相比宏来说,没有额外的开销,但更安全可靠。

10. 显式按需生成默认函数

C++会缺省生成一些默认函数,比如构造函数、拷贝构造函数等,有时候默认生成的规则还有点问题,建议的办法是当需要生成默认的构造函数时,需要增加=default,显示声明出来。

11. 善用shared_ptr

  1. shared_ptr要比uniq_ptr、原始指针大两倍,一个是原始指针,一个是指向控制块的指针。控制块用于定义引用计数、自定义删除器等,负责shared_ptr的析构操作。
  2. 避免用原始指针创建shared_ptr,可能会创建多个控制块,导致析构多次。不需要自定义删除器时,建议使用make_shared创建,代码更紧凑,规避资源泄漏风险。
  3. 如果想返回this指针的shared_ptr,需要继承std::enable_shared_from_this类,并使用shared_from_this方法获取shard_ptr,这是为了创建多个控制块。

12. 使用std::weak_ptr替代可能会悬空的std::shared_ptr

std::weak_ptr能检测到指针是否悬空 & 不负责管理指针的生命周期,潜在使用场景包括:缓存、观察者列表、打破std::shared_ptr环状引用。

13. 理解std::move

std::move不会移除const属性,因此避免移动const的对象,会产生const属性的右值引用,会被转成拷贝操作,而不是移动操作。

std::move仅仅做右值类型的转换,运行期间不作任何事情(不移动东西)。

14. std::move vs std::forward

std::move始终转换成右值,但是std::forward只有绑定右值属性时,才会被转发成右值,通常用于模板函数通用引用参数的转发。

15. 理解通用引用 & 右值引用

通用引用:如果函数模板形参的类型为T&&,并且T需要被推导得知,或者如果一个对象被声明为auto&&,这个形参或者对象就是一个通用引用。当参数绑定到右值时,表现为右值引用;当参数绑定为左值时,则为左值引用。

右值引用:声明为具体类型的Type&&,不涉及类型推导。

16. 避免对通用引用进行重载

通用引用形参的函数重载,会导致函数调用的机会比期望的多得多,容易出现误匹配的情况,导致bug产生。

比如构造函数的通用引用参数重载,对于non-const左值,它们比拷贝构造函数而更匹配,会劫持派生类对于基类的拷贝和移动构造函数的调用。

17. 移动操作也有一些局限性

待移动的对象没有提供移动操作,移动会退化成复制操作。
移动操作并不比复制快,比如字符串复制之类的。
移动方法不可用,没有被声明noexcept。
源对象是左值。

18. 熟悉完美转发失败的情况

模板类型推导失败,或者推导错误的类型时。
参数是花括号初始化,传入指针0/NULL等情况(推导出错误的类型)

19. 理解lambda

  1. 参数捕获时,避免悬空指针/引用:
    1.1 如果按引用捕获,避免出现引用对象生命周期消亡后继续被使用的情况,指针同理。
    1.2 C14支持移动捕获,lambda捕获参数列表,newPtr = std::move(pw),将所属权转移到
    1.3 C11可以使用std::bind模拟移动捕获。
  2. 支持通用引用泛型参数:使用decltype解析引用类型(左值 or 右值),然后使用std::forward进行完美转发。
  3. 优先使用lambda而不是bind,前者更易读、可维护、有效,只有C++11不支持移动捕获的情况才,才推荐使用std::bind。

20. 异步任务使用std::async而不是std::thread

  1. 代码更优雅:基于任务的代码量更少,能避免一些异常程序终止情况。
  2. 抽象层级更高:线程管理交给底层开发者,比如啥时候创建线程执行任务,避免资源超额、抛出异常、程序终止的问题。
    避免以默认参数调用std::async,因为这种情况导致func运行的所在线程不定,读取thread_local参数时,会有出错的可能性。比如明确新起一个线程,则指明std::launch::async模式(切到新线程)。

21. std::atomic & volatile

1)std::atomic用于在不使用互斥锁情况下,来使变量被多个线程访问的情况。是用来编写并发程序的一个工具,避免并发读取不一致的case。
2)volatile用在读取和写入不应被优化掉的内存上。是用来处理特殊内存的一个工具,避免内存读取操作被优化。